/*
 * Copyright (c) 2020 pig4cloud Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.love.cloud.auth.endpoint;

import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.love.cloud.admin.api.dto.UserInfo;
import com.love.cloud.admin.api.provider.RemoteUserProvider;
import com.love.cloud.common.core.constant.CacheConstants;
import com.love.cloud.common.core.constant.CommonConstants;
import com.love.cloud.common.core.constant.SecurityConstants;
import com.love.cloud.common.core.util.R;
import com.love.cloud.common.security.component.CustomAuthGeneratorFactory;
import com.love.cloud.common.security.properties.MySecurityTokenProperties;
import com.love.cloud.common.security.util.SecurityUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.ConvertingCursor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpHeaders;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.*;

/**
 * token终端
 */
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/token")
public class MyTokenEndpoint {

	private final ClientDetailsService clientDetailsService;

	private final TokenStore tokenStore;

	private final RedisTemplate redisTemplate;

	private final CacheManager cacheManager;

	private final MySecurityTokenProperties mySecurityTokenProperties;
	@DubboReference(version = "1.0.0", timeout = 30000)
	private RemoteUserProvider remoteUserProvider;

	/**
	 * 认证页面
	 * @param modelAndView
	 * @param error 表单登录失败处理回调的错误信息
	 * @return ModelAndView
	 */
	@GetMapping("/login")
	public ModelAndView require(ModelAndView modelAndView, @RequestParam(required = false) String error) {
		modelAndView.setViewName("ftl/login");
		modelAndView.addObject("error", error);
		return modelAndView;
	}

	/**
	 * 确认授权页面
	 * @param request
	 * @param session
	 * @param modelAndView
	 * @return
	 */
	@GetMapping("/confirm_access")
	public ModelAndView confirm(HttpServletRequest request, HttpSession session, ModelAndView modelAndView) {
		Map<String, Object> scopeList = (Map<String, Object>) request.getAttribute("scopes");
		modelAndView.addObject("scopeList", scopeList.keySet());

		Object auth = session.getAttribute("authorizationRequest");
		if (auth != null) {
			AuthorizationRequest authorizationRequest = (AuthorizationRequest) auth;
			ClientDetails clientDetails = clientDetailsService.loadClientByClientId(authorizationRequest.getClientId());
			modelAndView.addObject("app", clientDetails.getAdditionalInformation());
			modelAndView.addObject("user", SecurityUtils.getUser());
		}

		modelAndView.setViewName("ftl/confirm");
		return modelAndView;
	}

	/**
	 * 退出并删除token
	 * @param authHeader Authorization
	 */
	@DeleteMapping("/logout")
	public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
		if (StrUtil.isBlank(authHeader)) {
			return R.ok();
		}

		String tokenValue = authHeader.replace(OAuth2AccessToken.BEARER_TYPE, StrUtil.EMPTY).trim();
		return removeToken(tokenValue);
	}

	/**
	 * 令牌管理调用
	 * @param token token
	 */
	@DeleteMapping("/{token}")
	public R<Boolean> removeToken(@PathVariable("token") String token) {
		OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
		if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) {
			return R.failed("身份认证信息无效");
		}

		OAuth2Authentication auth2Authentication = tokenStore.readAuthentication(accessToken);
		// 清空用户信息
		cacheManager.getCache(CacheConstants.USER_DETAILS).evict(auth2Authentication.getName());

		// 清空access token
		tokenStore.removeAccessToken(accessToken);

		// 清空 refresh token
		OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
		tokenStore.removeRefreshToken(refreshToken);
		return R.ok(null,"删除成功");
	}

	/**
	 * 移除AccessToken
	 * @param token token
	 */
	@DeleteMapping("/removeAccessToken/{token}")
	public R<Boolean> removeAccessToken(@PathVariable("token") String token) {
		OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
		if (accessToken == null || StrUtil.isBlank(accessToken.getValue())) {
			return R.failed("身份认证信息无效");
		}
		// 清空access token
		tokenStore.removeAccessToken(accessToken);
		return R.ok(null,"删除成功");
	}

	/**
	 * 移除AccessToken /只允许rpc调用
	 */
	public void removeAllAccessToken() {
		String key = String.format("%sauth_to_access:*", CacheConstants.PROJECT_OAUTH_ACCESS);
		List<String> keys = this.findKeys(key);
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
		List list = redisTemplate.opsForValue().multiGet(keys);
		for (Object data :list) {
			if(data instanceof DefaultOAuth2AccessToken){
				DefaultOAuth2AccessToken defaultOAuth2AccessToken=(DefaultOAuth2AccessToken)data;
				String token = defaultOAuth2AccessToken.getValue();
				// 清空access token
				OAuth2AccessToken accessToken = tokenStore.readAccessToken(token);
				tokenStore.removeAccessToken(accessToken);
			}
		}
	}

	/**
	 * 设置最新的用户数据通过token
	 * @param token
	 */
	public void setNewUserInfoByToken(String token) {
		//UserInfo userInfo = remoteUserProvider.info((String) userName);
		//todo 后续实现，现在并无方案
	}


	/**
	 * 设置最新的用户数据通过token
	 * @param token
	 */
	public boolean setNewUserInfoByToken(String token,UserInfo userInfo){
		String key = String.format("%sauth:"+token, CacheConstants.PROJECT_OAUTH_ACCESS);
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
		Object o = redisTemplate.opsForValue().get(key);
		if(o instanceof OAuth2Authentication){
			OAuth2Authentication oAuth2Authentication=(OAuth2Authentication)o;
			String accesskey = CustomAuthGeneratorFactory.getGenerator(mySecurityTokenProperties.getUserOnlyTokenGenerator()).extractKey(oAuth2Authentication);
			Object data =redisTemplate.opsForValue().get(CacheConstants.PROJECT_OAUTH_ACCESS+"auth_to_access:"+accesskey);
			if(data instanceof DefaultOAuth2AccessToken){
				DefaultOAuth2AccessToken defaultOAuth2AccessToken=(DefaultOAuth2AccessToken)data;
				Object userName = defaultOAuth2AccessToken.getAdditionalInformation().get(SecurityConstants.DETAILS_USERNAME);
				if(userName !=null &&userName instanceof String){
					if(userInfo!=null){
						log.info("开始执行用户修改信息");
						Map<String, Object> additionalInfo = new HashMap<>(16);
						additionalInfo.put(SecurityConstants.DETAILS_LICENSE, SecurityConstants.PROJECT_LICENSE);
						additionalInfo.put(SecurityConstants.DETAILS_USER_ID, userInfo.getSysUser().getId());
						additionalInfo.put(SecurityConstants.DETAILS_USERNAME, userName);
						additionalInfo.put(SecurityConstants.DETAILS_DEPT_INFO, userInfo.getSysDeptList());
						log.info("获取到用户部门信息:"+JSON.toJSONString(userInfo.getSysDeptList()));
						additionalInfo.put(SecurityConstants.DETAILS_USERINFO, userInfo);
						Set<String> dbAuthsSet = new HashSet<>();
						if (ArrayUtil.isNotEmpty(userInfo.getRoles())) {
							// 获取角色
							userInfo.getRoles().forEach(role -> dbAuthsSet.add(SecurityConstants.ROLE + role.getRoleId()));
							// 获取资源
							dbAuthsSet.addAll(Arrays.asList(userInfo.getPermissions()));

						}
						additionalInfo.put("authorities",dbAuthsSet.toArray(new String[0]));
						defaultOAuth2AccessToken.setAdditionalInformation(additionalInfo);
						//更新token信息
						tokenStore.storeAccessToken(defaultOAuth2AccessToken, oAuth2Authentication);
						return true;
					}
				}
			}
		}
		return false;
	}

	/**
	 * 清除相关用户的客户端对应全部token
	 * @param userName
	 * @param clientIds
	 */
	public Object removeTokenByUserName(String userName, Set<String> clientIds){
		//更新对应客户端的token中用户信息
		for (String clientId :clientIds) {
			Collection<OAuth2AccessToken> oAuth2AccessTokenList = tokenStore.findTokensByClientIdAndUserName(clientId, userName);
			for (OAuth2AccessToken oAuth2AccessToken :oAuth2AccessTokenList) {
				this.removeToken(oAuth2AccessToken.getValue());
			}
		}
		return "清除成功";
	}

	/**
	 * 设置最新的用户数据通过用户名
	 * @param userName
	 */
	public void setNewUserInfoByUserName(String userName, Set<String> clientIds){
		//获取最新的用户详情
		UserInfo userInfo = remoteUserProvider.info((String) userName);
		log.info("获取的到用户信息::"+ JSON.toJSONString(userInfo));
		Set<String> updateToken=new HashSet<>();
		//更新对应客户端的token中用户信息
		for (String clientId :clientIds) {
			Collection<OAuth2AccessToken> oAuth2AccessTokenList = tokenStore.findTokensByClientIdAndUserName(clientId, userName);
			for (OAuth2AccessToken oAuth2AccessToken :oAuth2AccessTokenList) {
				String tokenValue = oAuth2AccessToken.getValue();
				if (updateToken.contains(tokenValue)) {
					continue;
				}
				log.info("获取到token数据::"+tokenValue);
				updateToken.add(tokenValue);
				boolean b = this.setNewUserInfoByToken(tokenValue,userInfo);
			}
		}
	}

	/**
	 * 查询token
	 * @param params 分页参数
	 * @return
	 */
	@PostMapping("/page")
	public R<Page> tokenList(@RequestBody Map<String, Object> params) {
		// 根据分页参数获取对应数据
		String key = String.format("%sauth_to_access:*", CacheConstants.PROJECT_OAUTH_ACCESS);
		List<String> pages = findKeysForPage(key, MapUtil.getInt(params, CommonConstants.CURRENT),
				MapUtil.getInt(params, CommonConstants.SIZE));

		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
		Page result = new Page(MapUtil.getInt(params, CommonConstants.CURRENT),
				MapUtil.getInt(params, CommonConstants.SIZE));
		result.setRecords(redisTemplate.opsForValue().multiGet(pages));
		result.setTotal(redisTemplate.keys(key).size());
		return R.ok(result);
	}

	/**
	 * 从缓存中获取到全部key
	 * @param patternKey
	 * @return
	 */
	private List<String> findKeys(String patternKey) {
		Cursor cursor = this.getCursor(patternKey);
		List<String> result = new ArrayList<>();
		assert cursor != null;
		while (cursor.hasNext()) {
			result.add(cursor.next().toString());
		}
		try {
			cursor.close();
		}
		catch (Exception e) {
			log.error("关闭cursor 失败");
		}
		return result;
	}

	/**
	 * 从缓存中获取到分页key
	 * @param patternKey
	 * @param pageNum
	 * @param pageSize
	 * @return
	 */
	private List<String> findKeysForPage(String patternKey, int pageNum, int pageSize) {
		Cursor cursor = this.getCursor(patternKey);
		List<String> result = new ArrayList<>();
		int tmpIndex = 0;
		int startIndex = (pageNum - 1) * pageSize;
		int end = pageNum * pageSize;

		assert cursor != null;
		while (cursor.hasNext()) {
			if (tmpIndex >= startIndex && tmpIndex < end) {
				result.add(cursor.next().toString());
				tmpIndex++;
				continue;
			}
			if (tmpIndex >= end) {
				break;
			}
			tmpIndex++;
			cursor.next();
		}

		try {
			cursor.close();
		}
		catch (Exception e) {
			log.error("关闭cursor 失败");
		}
		return result;
	}

	private Cursor getCursor(String patternKey){
		ScanOptions options = ScanOptions.scanOptions().count(1000L).match(patternKey).build();
		RedisSerializer<String> redisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
		return (Cursor) redisTemplate.executeWithStickyConnection(
				redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize));
	}

}
